package com.sunday.redis.lua.core;

import org.springframework.data.redis.core.script.RedisScript;

/**
 * @Description: redis lua
 * <p>
 * 脚本的原子性
 * Redis使用相同的Lua解释器来运行所有命令。另外，Redis保证以原子方式执行脚本：执行脚本时不会执行其他脚本或Redis命令。
 * 这种语义类似于MULTI / EXEC中的一种。从所有其他客户端的角度来看，脚本的效果还是不可见或已经完成。
 * 但是，这也意味着执行慢速脚本不是一个好主意。创建快速脚本并不难，因为脚本开销非常低，但是如果要使用慢速脚本，
 * 则应注意，在脚本运行时，没有其他客户端可以执行命令。
 * <p>
 * redis提供了以下几个script命令，用于对于脚本子系统进行控制：
 * <p>
 * script flush：清除所有的脚本缓存
 * <p>
 * script load：将脚本装入脚本缓存，不立即运行并返回其校验码
 * <p>
 * script exists：根据指定脚本校验码，检查脚本是否存在于缓存
 * <p>
 * script kill：杀死当前正在运行的脚本（防止脚本运行缓存，占用内存）
 * <p>
 * 验证 redis在执行 本身命令与lua脚本，都将互斥
 * localhost:0>eval "while 1 do end" 0  //进行脚本的死锁，测试其他命令是否同时可用
 * localhost:0>get a    //执行redis命令，将会死锁
 * "BUSY Redis is busy running a script. You can only call SCRIPT KILL or SHUTDOWN NOSAVE."
 * localhost:0>script kill  //执行kill命令 此时 eval 报错已关闭的提示
 * "ERR Error running script (call to f_a0aa8d656b65882e2a8f491fbb37ba7fa649be34):
 * @user_script:1: Script killed by user with SCRIPT KILL... "
 * <p>
 * 脚本缓存语义
 * 确保已执行的脚本永远位于Redis实例的给定执行的脚本缓存中。这意味着，如果对Redis实例执行EVAL，则所有后续的EVALSHA调用都会成功。
 * 可以长时间缓存脚本的原因是，编写良好的应用程序不太可能具有足够多的不同脚本来导致内存问题。从概念上讲，每个脚本都类似于新命令的实现，即使是大型应用程序也可能只有数百个。即使对应用程序进行了多次修改并且脚本将更改，使用的内存也可以忽略不计。
 * 刷新脚本缓存的唯一方法是显式调用SCRIPT FLUSH命令，该命令将完全刷新脚本缓存，从而删除到目前为止已执行的所有脚本。
 * <p>
 * PS：再次建议，现在百度能查到东西，很多都有误导倾向，或者没有完全理解的人写的
 * https://redis.io/commands/eval#script-cache-semantics
 * 建议直接查看官网,可以配合百度的例子来看，最终都需要进行官网文档的更正！！！务必最终查看的是官方文档
 * @Author: zsy
 * @Create: 2022/10/17 17:58
 */
public class RedisScriptLua {

    /**
     * 指定数量累加,并判定上限
     * ---> 到达上限返回null，否则返回累加后的值
     * 场景: 用户部分功能的数量限制，防止设置超限
     * <p>
     * eval "local count = redis.call('get', KEYS[1]); if count == false then count = 0; end; if tonumber(ARGV[2]) >= tonumber(ARGV[1]) + count then return redis.call('incrby', KEYS[1], ARGV[1]); end; return nil;" 1 userId 1 2
     * <p>
     * 1. key[1] {KEYS[1] 检索的key}
     * 2. args [2] {ARGV[1] 数量, ARGV[2] 上限}
     */
    public final static RedisScript<Long> INCREMENT_WITH_LIMIT = RedisScript.of(
            """
                    local count = redis.call('get', KEYS[1]);\s\
                    if count == false then\s\
                        count = 0;\s\
                    end;\s\
                    if tonumber(ARGV[2]) >= tonumber(ARGV[1]) + count then\s\
                        return redis.call('incrby', KEYS[1], ARGV[1]);\s\
                    end;\s\
                    return nil;
                    """,
            Long.class);

    /**
     * 指定数量扣减,并判定下限
     * ---> 到达下限返回null，否则返回递减后的值
     * 场景: 用户余额指定数量扣减
     * <p>
     * eval "local count = redis.call('get', KEYS[1]); if count ~= false and count - tonumber(ARGV[1]) >= tonumber(ARGV[2]) then return redis.call('decrby', KEYS[1], ARGV[1]); end; return nil;" 1 userId 1 0
     * <p>
     * 1. key[1] {KEYS[1] 检索的key}
     * 2. args [2] {ARGV[1] 数量, ARGV[2] 下限}
     */
    public final static RedisScript<Long> DECREMENT_WITH_FLOOR = RedisScript.of(
            """
                    local count = redis.call('get', KEYS[1]);\s\
                    if count ~= false and count - tonumber(ARGV[1]) >= tonumber(ARGV[2]) then\s\
                        return redis.call('decrby', KEYS[1], ARGV[1]);\s\
                    end;\s\
                    return nil;
                    """,
            Long.class);

    /**
     * @Description: 在指定时间范围内按照指定数量累加，并判定上限
     * ---> 返回应休眠的时间（ms毫秒），返回0未超限无需休眠
     * <p>
     * 场景: 分布式消息递送的频次限制，请求的TPS限制（网关拉模式下，速率控制）
     * <p>
     * https://redis.io/
     * https://redis.io/docs/manual/transactions/
     * https://redis.io/docs/manual/programmability/
     * https://redis.io/docs/manual/keyspace-notifications/
     * <p>
     * 运行脚本或函数时，Redis 保证其原子执行。 脚本的执行在整个过程中阻止所有服务器活动，类似于事务的语义。
     * Redis 确保 使用单个 write 系统调用将事务写入磁盘。
     * 过期事件的计时, Redis 以两种方式使具有生存时间关联的密钥过期
     * 当命令访问密钥并发现密钥已过期时。
     * 通过后台系统，在后台以增量方式查找过期密钥，以便能够收集从未访问过的密钥。
     * 具体参考Redisson中的实现，并没有对ttl的执行进行补偿策略
     * <p>
     * 控限制 返回 pttl (ms毫秒) 过期时间
     * 根据上面的信息分析其中需要特殊注意 一. 二.是不可能发生的，因为同一时间没有人能清理掉已经过期的key，动作都是暂停的
     * 一. pttl == -2 key不存在时返回
     * 二. pttl == -1 key永久存在时返回
     * 三. pttl == 0  是最常见的，当过期时间处于0的时候，避免系统高速循环，所以进行了 1毫秒的返回。
     * 四. 程序在高速运行的时候，可能大量产生 三 的情况（获取 pttl = 0 的情况）, 在测试过程中，大概发生概率高达20%左右，因此考虑到 redis的访问频次的耗损，因此在整个lua中 pttl = 0 时均进行返回 1 毫秒休眠时间操作，降低redis的访问频次
     * <p>
     * eval "local count = redis.call('get', KEYS[1]);  if count == false then  count = 0;  redis.call('set', KEYS[1], count, 'nx', 'ex', ARGV[3]);  end;  if tonumber(ARGV[2]) >= tonumber(ARGV[1]) + count then  redis.call('incrby', KEYS[1], ARGV[1]);  return 0;  else  local pttl = redis.call('pttl', KEYS[1]);  if pttl == 0 then  return 1;  end;  if pttl == -1 then  redis.call('expire', KEYS[1], ARGV[3]);  return ARGV[3] * 1000;  end;  return pttl;  end;" 1 userId 1 2 10
     * <p>
     * 1. key [1] {KEYS[1] 检索的key}
     * 2. args [3] {ARGV[1] 数量, ARGV[2] 上限, ARGV[3] 过期时间/S }
     */
    public final static RedisScript<Long> INCREMENT_WITHIN_PERIOD = RedisScript.of(
            """
                    local count = redis.call('get', KEYS[1]);\s\
                    if count == false then\s\
                        count = 0;\s\
                        redis.call('set', KEYS[1], count, 'nx', 'ex', ARGV[3]);\s\
                    end;\s\
                    if tonumber(ARGV[2]) >= tonumber(ARGV[1]) + count then\s\
                        redis.call('incrby', KEYS[1], ARGV[1]);\s\
                        return 0;\s\
                    else\s\
                        local pttl = redis.call('pttl', KEYS[1]);\s\
                        if pttl == 0 then\s\
                            return 1;\s\
                        end;\s\
                        if pttl == -1 then\s\
                            redis.call('expire', KEYS[1], ARGV[3]);\s\
                            return ARGV[3] * 1000;\s\
                        end;\s\
                        return pttl;\s\
                    end;
                    """,
            Long.class);


    /**
     * @Description: 在指定时间范围内按照指定数量累加，并判定上限 (简易版，不进行TTL控制)
     * ---> 返回上限情况，true正常累加，false超限
     * <p>
     * 场景: 分布式消息递送的频次限制，请求的TPS限制 （用户访问频次控制）
     * <p>
     * eval "local count = redis.call('get', KEYS[1]); if count == false then count = 0; redis.call('set', KEYS[1], count, 'nx', 'ex', ARGV[3]); end; if tonumber(ARGV[2]) >= tonumber(ARGV[1]) + count then redis.call('incrby', KEYS[1], ARGV[1]); return 1; end; return -1;" 1 userId 1 2 10
     * <p>
     * 1. key [1] {KEYS[1] 检索的key}
     * 2. args [3] {ARGV[1] 数量, ARGV[2] 上限, ARGV[3] 过期时间/S }
     */
    public final static RedisScript<Boolean> EASY_INCREMENT_WITHIN_PERIOD = RedisScript.of(
            """
                    local count = redis.call('get', KEYS[1]);\s\
                    if count == false then\s\
                        count = 0;\s\
                        redis.call('set', KEYS[1], count, 'nx', 'ex', ARGV[3]);\s\
                    end;\s\
                    if tonumber(ARGV[2]) >= tonumber(ARGV[1]) + count then\s\
                        redis.call('incrby', KEYS[1], ARGV[1]);\s\
                        return 1;\s\
                    end;\s\
                    return -1;\s\
                    """,
            Boolean.class);


    /**
     * @Description: 在期限内累加，返回当前计数数量
     * ---> 返回当前计数数量
     * 场景: 仅判定累加数量，不进行额外的控制，其他由业务自行判定
     * <p>
     * eval "local count = redis.call('get', KEYS[1]); if count == false then redis.call('incr', KEYS[1]); redis.call('expire', KEYS[1], ARGV[1]); return 1; end; return redis.call('incr', KEYS[1]);" 1 userId 10
     * <p>
     * 1. key [1] {KEYS[1] 检索的key}
     * 2. args [3] {ARGV[1] 数量, ARGV[2] 下限, ARGV[3] 限制时间范围/MS }
     */
    public final static RedisScript<Long> INCR_WITHIN_PERIOD = RedisScript.of(
            """
                    local count = redis.call('get', KEYS[1]);\s\
                    if count == false then\s\
                        redis.call('incr', KEYS[1]);\s\
                        redis.call('expire', KEYS[1], ARGV[1]);\s\
                        return 1;\s\
                    end;\s\
                    return redis.call('incr', KEYS[1]);\s\
                    """,
            Long.class);

    /**
     * @Description: 在符合指定时间范围内递减，最小操作时间范围 ARGV[3]
     * ---> 返回下限情况，true正常递减，false超限
     * 场景: 分布式消息递送的频次,发生异常时,计数归还动作
     * <p>
     * eval "local count = redis.call('get', KEYS[1]); if count ~= false and count - tonumber(ARGV[1]) >= tonumber(ARGV[2]) then local pttl = redis.call('pttl',KEYS[1]); if pttl >= tonumber(ARGV[3]) then redis.call('decrby', KEYS[1], ARGV[1]); return 1; end; end; return -1;" 1 userId 1 0 20
     * <p>
     * 1. key [1] {KEYS[1] 检索的key}
     * 2. args [3] {ARGV[1] 数量, ARGV[2] 下限, ARGV[3] 限制时间范围/MS }
     */
    public final static RedisScript<Boolean> DECREMENT_WITHIN_PERIOD = RedisScript.of(
            """
                    local count = redis.call('get', KEYS[1]);\s\
                    if count ~= false and count - tonumber(ARGV[1]) >= tonumber(ARGV[2]) then\s\
                        local pttl = redis.call('pttl',KEYS[1]);\s\
                        if pttl >= tonumber(ARGV[3]) then\s\
                            redis.call('decrby', KEYS[1], ARGV[1]);\s\
                            return 1;\s\
                        end;\s\
                    end;\s\
                    return -1;
                    """,
            Boolean.class);

    /**
     * 补充一个知识点
     * https://redis.io/docs/latest/operate/oss_and_stack/reference/cluster-spec/
     * 在Redis Cluster中，使用花括号 {} 中的字符串来指导键分片。
     * 如果键包含由花括号 {} 包围的部分，只有花括号内的字符串会被用来计算键应该落在哪个槽上。
     * 例如对于键 {abc}def，只有 abc 会被用来计算哈希，以决定键应该保存在哪个槽中。
     * 这样可以确保具有相同花括号内内容的所有键会被分配到同一个槽中，这对于需要进行多键操作的场景特别重要，因为Redis要求涉及多个键的操作必须在同一个槽上进行。
     */

    /**
     * @Description: 分时段速率控制 (分钟/小时/天)
     * ---> 返回 0 有效, 1 每分钟上限, 2 每小时上限, 3 每天上限
     * 使用场景：例如在指定时间内，手机号码超过最大次数，不考虑次数累加带来的影响，并返回当前计数数量
     * <p>
     * 将参数进行递增 1 ,如果key没有超时时间并添加时间
     * eval "local minute = '{'..KEYS[1]..'}_MINUTE'; local current = redis.call('incr', minute); if redis.call('pttl',minute) == -1 then redis.call('expire', minute, 60); end; if current > tonumber(ARGV[1]) then return 1; end; local hour = '{'..KEYS[1]..'}_HOUR'; current = redis.call('incr', hour); if redis.call('pttl',hour) == -1 then redis.call('expire',hour, 3600); end; if current > tonumber(ARGV[2]) then return 2; end; local day = '{'..KEYS[1]..'}_DAY'; current = redis.call('incr', day); if redis.call('pttl',day) == -1 then redis.call('expire',day, 86400); end; if current > tonumber(ARGV[3]) then return 3; end; return 0;" 1 userId 1 2 3
     * <p>
     * 1. key [1] {KEYS[1] key }
     * 2. args [3] {ARGV[1] 每分钟上限, ARGV[2] 每小时上限, ARGV[3] 每天上限 }
     */
    public final static RedisScript<Long> CALCULATE_SEGMENT_RATE = RedisScript.of(
            //优先判定每分钟的速率
            //优先判定每小时的速率
            //优先判定每天的速率
            """
                     local minute = '{'..KEYS[1]..'}_MINUTE';\s\
                     local current = redis.call('incr', minute);\s\
                     if redis.call('pttl',minute) == -1 then\s\
                         redis.call('expire', minute, 60);\s\
                     end;\s\
                     if current > tonumber(ARGV[1]) then\s\
                         return 1;\s\
                     end;\s\
                     local hour = '{'..KEYS[1]..'}_HOUR';\s\
                     current = redis.call('incr', hour);\s\
                     if redis.call('pttl',hour) == -1 then\s\
                         redis.call('expire',hour, 3600);\s\
                     end;\s\
                     if current > tonumber(ARGV[2]) then\s\
                         return 2;\s\
                     end;\s\
                     local day = '{'..KEYS[1]..'}_DAY';\s\
                     current = redis.call('incr', day);\s\
                     if redis.call('pttl',day) == -1 then\s\
                         redis.call('expire',day, 86400);\s\
                     end;\s\
                     if current > tonumber(ARGV[3]) then\s\
                         return 3;\s\
                     end;\s\
                     return 0;
                    """,
            Long.class);

    /**
     * @Description: 递增并判定上限，每次都刷新过期时间
     * ---> 返回是否超限，true未超限，false超限
     * 场景：下载计数，每次刷新过期时间
     * eval "local current = redis.call('incr',KEYS[1]); redis.call('expire', KEYS[1], ARGV[2]); if current > tonumber(ARGV[1]) then return -1; end; return 1;" 1 abc 1 10
     * <p>
     * 1. key [1] {KEYS[1] 检索的key}
     * 2. args [2] {ARGV[1] 上限, ARGV[2] 过期时间/S}
     */
    public final static RedisScript<Boolean> INCR_CHECK_MAX_AND_REFRESH = RedisScript.of(
            """
                    local current = redis.call('incr',KEYS[1]);\s\
                    redis.call('expire', KEYS[1], ARGV[2]);\s\
                    if current > tonumber(ARGV[1]) then\s\
                        return -1;\s\
                    end;\s\
                    return 1;
                    """,
            Boolean.class);

    /**
     * @Description: 更新值，不更新过期时间
     * ---> 返回剩余过期时间(ms毫秒)
     * 场景： 仅更新值内容
     * eval "local pttl = redis.call('pttl', KEYS[1]); redis.call('set', KEYS[1], ARGV[1]); if 0 > pttl then pttl = ARGV[2] * 1000; end; redis.call('pexpire', KEYS[1], pttl); return pttl;" 1 a b 10
     * <p>
     * 1. key [1] {KEYS[1] 检索的key}
     * 2. args [2] {ARGV[1] 值, ARGV[2] 过期时间/S}
     */
    public final static RedisScript<Long> UPDATE_VALUE_KEEP_TTL = RedisScript.of(
            """
                    local pttl = redis.call('pttl', KEYS[1]);\s\
                    redis.call('set', KEYS[1], ARGV[1]);\s\
                    if 0 > pttl then\s\
                        pttl = ARGV[2] * 1000;\s\
                    end;\s\
                    redis.call('pexpire', KEYS[1], pttl);\s\
                    return pttl;
                    """,
            Long.class);

    /**
     * @Description: 值相同刷新过期时间（key 不存在设置失败）
     * ---> 返回是否刷新成功，true刷新成功，false刷新失败
     * 场景： 延长数据的存活时间，排他锁能力
     * eval "local value = redis.call('get',KEYS[1]); if value ~= false and value == ARGV[1] then return redis.call('expire', KEYS[1], ARGV[2]); end; return -1;" 1 a b 30
     * <p>
     * 1. key [1] {KEYS[1] 检索的key}
     * 2. args [2] {ARGV[1] 值, ARGV[2] 过期时间/S}
     */
    public final static RedisScript<Boolean> VALUE_EQ_AND_EXPIRE = RedisScript.of(
            """
                    local value = redis.call('get',KEYS[1]);\s\
                    if value ~= false and value == ARGV[1] then\s\
                        return redis.call('expire', KEYS[1], ARGV[2]);\s\
                    end;\s\
                    return -1;
                    """,
            Boolean.class);

}
